package org.mijia.dotsbox.core;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;

import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.BooleanClause;
import org.apache.lucene.search.BooleanQuery;
import org.apache.lucene.search.Filter;
import org.apache.lucene.search.IndexSearcher;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.Sort;
import org.apache.lucene.search.TermQuery;
import org.apache.lucene.search.TopFieldDocs;
import org.mijia.dotsbox.core.model.Entity;
import org.mijia.dotsbox.core.search.ReferCounter;
import org.mijia.dotsbox.core.search.SearchBlock;
import org.mijia.dotsbox.utils.LogUtils;

public class EntityManager {

	private static final int MAX_COUNT_FINDALL = 100;
	
	private SearchBlock sBlock;
	private HashMap<String, List<Method>> metaInfo = new HashMap<String, List<Method>>(5);
	
	public EntityManager(File dataDir) {
		Properties searchProp = new Properties();
		InputStream is = getClass().getResourceAsStream("search.properties");
		if (is == null) {
			LogUtils.warning("search.properties not exists!");
		} else {
			try {
				searchProp.load(is);
			} catch (IOException e) {
				LogUtils.warning("Cannot load search properties!" + e.getMessage());
			} finally {
				try {
					is.close();
				} catch (IOException e) {
				}
			}
		}
		sBlock = new SearchBlock(dataDir, searchProp);
	}
	
	@SuppressWarnings("unchecked")
	public Entity find(Class persistentClass, Object key) {
		Query idTerm = new TermQuery(new Term("id", (String) key));
		Query clazzTerm = new TermQuery(new Term("clazz", persistentClass.getName()));
		BooleanQuery query = new BooleanQuery();
		query.add(idTerm, BooleanClause.Occur.MUST);
		query.add(clazzTerm, BooleanClause.Occur.MUST);
		TopFieldDocs docs = getResult(query, null, 1, Sort.RELEVANCE);
		if (docs != null && docs.scoreDocs != null && docs.scoreDocs.length > 0) {
			int nDoc = docs.scoreDocs[0].doc;
			Document doc = sBlock.getDocument(nDoc);
			if (doc != null) {
				Entity entity = fromDocumentToEntity(doc);
				return entity;
			}
		}
		return null;
	}
	
	@SuppressWarnings("unchecked")
	public List<Entity> findAll(Class persistentClass) {
		List<Entity> results = new ArrayList<Entity>();
		Query query = new TermQuery(new Term("clazz", persistentClass.getName()));
		TopFieldDocs docs = getResult(query, null, MAX_COUNT_FINDALL, Sort.RELEVANCE);
		if (docs != null && docs.scoreDocs != null && docs.scoreDocs.length > 0) {
			int nDoc = docs.scoreDocs[0].doc;
			Document doc = sBlock.getDocument(nDoc);
			if (doc != null) {
				Entity entity = fromDocumentToEntity(doc);
				results.add(entity);
			}
		}
		return results;
	}
	
	public List<String> getIds(Query query, Sort sort) {
		if (sort == null) {
			sort = Sort.RELEVANCE;
		}
		
		TopFieldDocs docs = getResult(query, null, MAX_COUNT_FINDALL, sort);
		int totalHits = docs.totalHits;
		if (totalHits > MAX_COUNT_FINDALL) {
			docs = getResult(query, null, totalHits, sort);
		}
		
		List<String> ids = new ArrayList<String>();
		if (docs != null && docs.scoreDocs != null) {
			for (int i = 0; i < docs.scoreDocs.length; ++i) {
				int nDoc = docs.scoreDocs[i].doc;
				Document doc = sBlock.getDocument(nDoc);
				if (doc != null) {
					String id = doc.get("id");
					ids.add(id);
				}
			}
		}
		return ids;
	}
	
	public Entity update(Entity entity) {
		Entity oldObject = find(entity.getClass(), entity.getId());
		if (oldObject != null) {
			if (oldObject.equals(entity)) {
				return oldObject;
			} 
			
			remove(entity.getClass(), entity.getId());
		}
		
		Document doc = fromEntityToDocument(entity);
		sBlock.addDocument(doc);
		
		// should return old entity object
		return oldObject;
	}
	
	@SuppressWarnings("unchecked")
	public void remove(Class persistentClass, Object key) {
		Query idTerm = new TermQuery(new Term("id", (String) key));
		Query clazzTerm = new TermQuery(new Term("clazz", persistentClass.getName()));
		BooleanQuery query = new BooleanQuery();
		query.add(idTerm, BooleanClause.Occur.MUST);
		query.add(clazzTerm, BooleanClause.Occur.MUST);
		TopFieldDocs docs = getResult(query, null, 1, Sort.RELEVANCE);
		if (docs != null && docs.scoreDocs != null && docs.scoreDocs.length > 0) {
			int nDoc = docs.scoreDocs[0].doc;
			sBlock.deleteDocument(nDoc);
		}
	}
	
	public void dispose() {
		if (sBlock != null) {
			sBlock.dispose();
			sBlock = null;
		}
		
		metaInfo.clear();
	}
	
	private Entity fromDocumentToEntity(Document doc) {
		String id = doc.get("id");
		String clazz = doc.get("clazz");
		try {
			Class<?> entityClass = Class.forName(clazz);
			Entity entity = (Entity) entityClass.newInstance();
			entity.setId(id);
			
			List<Method> methods = initMetaInfo(entity);
			for (Method m : methods) {
				EntityField meta = m.getAnnotation(EntityField.class);
				String name = meta.name();
				String setname = "set" + name.substring(0, 1).toUpperCase() + name.substring(1);
				String valueType = meta.valueType();
				Method setMethod = entityClass.getMethod(setname, new Class[] { 
						Class.forName(valueType)
				});
				if (setMethod != null) {
					String fieldValue = doc.get(name);
					// FIXME: handle java.util.Date;
					if (fieldValue != null && valueType.equals(String.class.getName())) {
						setMethod.invoke(entity, doc.get(name));
					}
				}
			}
			
			System.out.println(entity);
			return entity;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return null;
	}
	
	private Document fromEntityToDocument(Entity entity) {
		Document doc = new Document();
		doc.add(new Field("id", entity.getId(), Field.Store.YES, Field.Index.UN_TOKENIZED));
		doc.add(new Field("clazz", entity.getClass().getName(), Field.Store.YES, Field.Index.UN_TOKENIZED));
		
		List<Method> methods = initMetaInfo(entity);
		for (Method m : methods) {
			EntityField meta = m.getAnnotation(EntityField.class);
			Field.Store store = Field.Store.YES;
			switch (meta.store()) {
			case EntityField.STORE_NO:
				store = Field.Store.NO;
				break;
			case EntityField.STORE_YES:
				store = Field.Store.YES;
				break;
			case EntityField.STORE_COMPRESS:
				store = Field.Store.COMPRESS;
				break;
			}
			
			Field.Index index = Field.Index.NO;
			switch (meta.index()) {
			case EntityField.INDEX_NO:
				index = Field.Index.NO;
				break;
			case EntityField.INDEX_TOKENNIZE:
				index = Field.Index.TOKENIZED;
				break;
			case EntityField.INDEX_UNTOKENIZE:
				index = Field.Index.UN_TOKENIZED;
				break;
			}
			
			try {
				Object result = m.invoke(entity, new Object[0]);
				if (result != null) {
					// FIXME: get value according to valueType; handle java.util.Date
					String strValue = result.toString();
					doc.add(new Field(meta.name(), strValue, store, index));
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return doc;
	}
	
	private synchronized List<Method> initMetaInfo(Entity entity) {
		List<Method> methods = metaInfo.get(entity.getClass().getName());
		if (methods != null)
			return methods;
		
		methods = new ArrayList<Method>();
		for (Method m : entity.getClass().getMethods()) {
			if (m.isAnnotationPresent(EntityField.class)) {
				methods.add(m);
			}
		}
		metaInfo.put(entity.getClass().getName(), methods);
		return methods;
	}
	
	private TopFieldDocs getResult(Query query, Filter filter, int nCount, Sort sort) {
		ReferCounter<IndexSearcher> holder = sBlock.getIndexSearcher();
		if (holder == null)
			return null;
		
		IndexSearcher searcher = holder.get();
		TopFieldDocs docs = null;
		try {
			docs = searcher.search(query, filter, nCount, sort);
		} catch (IOException e) {
			e.printStackTrace();
			docs = null;
		}
		
		try {
			holder.decre();
		} catch (Exception e) {
			e.printStackTrace();
		}
		return docs;
	}
}
